SpringBoot(五)单元测试(Unit Test)

在开发工作中,通常写好代码后我们都会先自测一遍再交给测试部门,自测的方法有多种,也有多种测试工具,比如Postman、Jmeter等,这篇文章主要讲对于SpringBoot项目如何使用SpringBoot的单元测试,使用的SpringBoot版本是1.5.7。

一.示例项目介绍

创建一个SpringBoot的Maven项目,我的项目结构为:
项目结构图

SpringBoot的单元测试需要额外添加的依赖是:

1
2
3
4
5
6
7
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

//其它依赖此处省略...

下面给出项目的代码部分。
Javabean类:Book.java

1
2
3
4
5
6
7
8
9
10
11
package com.lzumetal.springboot.demodatabase.entity;

public class Book {

private Integer id; //数据库主键id标识
private String name; //书名
private String author; //作者
private Double price; //价格

//get、set方法省略
}

dao类:BookMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.lzumetal.springboot.demodatabase.mapper;


import com.lzumetal.springboot.demodatabase.entity.Book;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface BookMapper {

int insert(Book record);
List<Book> selectAll();
Book getById(@Param(value = "id") Integer id);
}

对应的xml映射文件:BookMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lzumetal.springboot.demodatabase.mapper.BookMapper">
<resultMap id="BaseResultMap" type="Book">
<result column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="author" jdbcType="VARCHAR" property="author" />
<result column="price" jdbcType="DOUBLE" property="price" />
</resultMap>
<insert id="insert" parameterType="Book">
insert into book (id, name, author,
price)
values (#{id,jdbcType=INTEGER}, #{name,jdbcType=VARCHAR}, #{author,jdbcType=VARCHAR},
#{price,jdbcType=DOUBLE})
</insert>
<select id="selectAll" resultMap="BaseResultMap">
select id, name, author, price
from book
</select>
<select id="getById" resultMap="BaseResultMap">
select id, name, author, price
from book
WHERE id = #{id}
</select>
</mapper>

Service类:BookServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.lzumetal.springboot.demodatabase.service;

import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.mapper.BookMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* Created by liaosi on 2017/9/26.
*/
@Service
public class BookService {

@Autowired
private BookMapper bookMapper;

public List<Book> getAllBooks() {
return bookMapper.selectAll();
}

public Book getById(Integer id) {
return bookMapper.getById(id);
}
}

Controller类:BookController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.lzumetal.springboot.demodatabase.controller;

import com.google.gson.Gson;
import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* Created by liaosi on 2017/9/26.
*/
@RestController
public class BookController {

private static Gson gson = new Gson();

@Autowired
private BookService bookService;

/**
* GET请求+@PathVariable
* @param id
* @return
*/
@RequestMapping(value = "/getBook/{id}", method = RequestMethod.GET)
public String getBookInfo(@PathVariable("id") Integer id) {
return gson.toJson(bookService.getById(id));
}


/**
* GET请求
* @param id
* @return
*/
@RequestMapping(value = "/getBookInfo2", method = RequestMethod.GET)
public String getBoodInfo2(Integer id, String name) {
Book book = new Book();
book.setId(id);
book.setName(name);
return gson.toJson(book);
}


/**
* 普通form表单POST请求
* @param id
* @return
*/
@RequestMapping(value = "/postBookInfo", method = RequestMethod.POST)
public String postBoodInfo(Integer id) {
return gson.toJson(bookService.getById(id));
}


/**
* POST请求,参数为json格式
* @param book
* @return
*/
@RequestMapping(value = "/postJson", method = RequestMethod.POST)
public Book postJson(@RequestBody Book book) {
return book;
}

}

SpringBoot项目的启动类:StartupApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.lzumetal.springboot.demodatabase;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// mapper 接口类包扫描
@MapperScan(basePackages = "com.lzumetal.springboot.demodatabase.mapper")
public class StartupApplication {

public static void main(String[] args) {
SpringApplication.run(StartupApplication.class, args);
}
}

二.测试类

参考官网文档:Testing improvements in Spring Boot 1.4

1.测试Service或者Controller

MainTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

package com.lzumetal.springboot.demodatabase.test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lzumetal.springboot.demodatabase.StartupApplication;
import com.lzumetal.springboot.demodatabase.controller.BookController;
import com.lzumetal.springboot.demodatabase.entity.Book;
import com.lzumetal.springboot.demodatabase.service.BookService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

/*
https://spring.io/blog/2016/04/15/testing-improvements-in-spring-boot-1-4
MOCK —提供一个Mock的Servlet环境,内置的Servlet容器并没有真实的启动,主要搭配使用@AutoConfigureMockMvc

RANDOM_PORT — 提供一个真实的Servlet环境,也就是说会启动内置容器,然后使用的是随机端口
DEFINED_PORT — 这个配置也是提供一个真实的Servlet环境,使用的默认的端口,如果没有配置就是8080
NONE — 这是个神奇的配置,跟Mock一样也不提供真实的Servlet环境。
*/

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartupApplication.class)
@ActiveProfiles(value = "dev")
public class MainTest {

private static Gson gson = new GsonBuilder().setPrettyPrinting().create();

@Autowired
private BookService bookService;

@Autowired
private BookController bookController;


@Test
public void testBookService() {
List<Book> allBooks = bookService.getAllBooks();
System.out.println(gson.toJson(allBooks));
}

@Test
public void testBookController() {
String s = bookController.getBookInfo(1);
System.out.println(s);
}

}

官网上的说明:

@RunWith(SpringRunner.class) tells JUnit to run using Spring’s testing support. SpringRunner is the new name for SpringJUnit4ClassRunner, it’s just a bit easier on the eye.
@SpringBootTest is saying “bootstrap with Spring Boot’s support” (e.g. load application.properties and give me all the Spring Boot goodness)
The webEnvironment attribute allows specific “web environments” to be configured for the test. You can start tests with a MOCK servlet environment or with a real HTTP server running on either a RANDOM_PORT or a DEFINED_PORT.
If we want to load a specific configuration, we can use the classes attribute of @SpringBootTest. In this example, we’ve omitted classes which means that the test will first attempt to load @Configuration from any inner-classes, and if that fails, it will search for your primary @SpringBootApplication class.

  • @RunWith 是junit提供的注解,表示该类是单元测试的执行类
  • SpringRunner是spring-test提供的测试执行单元类(是Spring单元测试中SpringJUnit4ClassRunner的新名字)
  • SpringBootTest 是执行测试程序的引导类。
  • @ActiveProfiles是用来指定激活的配置环境。

2.模拟发送REST请求测试

RestTemplate

在java代码里进行REST请求测试,常用的比如Apache的HttpClient,但是spring也提供了一种简单便捷的模板类RestTemplate来进行操作。

RestTemplate是Spring提供的一个web层测试模板类,通过RestTemplate可以很方便地进行web层功能测试。它支持REST风格的URL,而且具有AnnotationMethodHandlerAdapter的数据转换器HttpMessageConverters的装配功能。RestTemplate已默认帮我们完成了一下数据转换器的注册:

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • XmlAwareFormHttpMessageConverter

在默认情况下,我们可以直接利用以上转换器对响应数据进行转换处理。如StringHttpMessageConverter来处理text/plain;MappingJackson2HttpMessageConverter来处理application/json;MappingJackson2XmlHttpMessageConverter来处理application/xml

而如果我们像拓展其他的转换器如Jaxb2RootElementHttpMessageConverter或MappingJacksonHttpMessageConverter。我们可以使用setMessageConverters(List<HttpMessageConverter<?>> messageConverters)来注册我们所需的转换器。

1
2
3
4
5
6
7
RestTemplate restTemplate = new RestTemplate();
//获取RestTemplate默认配置好的所有转换器
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
//默认的MappingJackson2HttpMessageConverter在第7个 先把它移除掉
messageConverters.remove(6);
//添加上GSON的转换器
messageConverters.add(6, new GsonHttpMessageConverter());

这个简单的例子展示了如何使用GsonHttpMessageConverter替换掉默认用来处理application/json的MappingJackson2HttpMessageConverter。

RestTemplate默认(即使用无参构造器创建实例)是使用java.net包中的标准Java类作为底层实现来创建HTTP请求。但是可以调用它的带ClientHttpRequestFactory参数的构造器,使用 Apache 的 HttpComponents 或 Netty 和 OkHttp等其它HTTP请求库。

配置默认实例:

1
2
3
4
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}

配置定制实例,构造方法中可以传入ClientHttpRequestFactory参数,ClientHttpRequestFactory接口的实现类中存在timeout属性等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
RestTemplate restTemplate(){
//生成一个设置了连接超时时间、请求超时时间、异常最大重试次数的httpClient
RequestConfig config = RequestConfig.custom()
.setConnectionRequestTimeout(10000)
.setConnectTimeout(10000)
.setSocketTimeout(30000)
.build();
HttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(config)
.setRetryHandler(new DefaultHttpRequestRetryHandler(5, false))
.build();

//使用httpClient创建一个ClientHttpRequestFactory的实现
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);

//ClientHttpRequestFactory作为参数构造一个使用作为底层的RestTemplate
RestTemplate restTemplate = new RestTemplate(requestFactory);

TestRestTemplate

TestRestTemplate是SpringBoot提供的一个测试模板类,在SpringBoot你既可以使用RestTemplate,同时也可以使用TestRestTemplate,TestRestTemplate是RestTemplate的一个包装类,而没有继承它,所以不会存在bean注入的问题。如果想在TestRestTemplate中获取,可以调用它的getRestTemplate()方法。在使用了SpringBootTest注解的情况下,TestRestTemplate可以直接使用@Autowired注入。

在下面的示例中主要介绍如何使用TestRestTemplate进行post和get请求测试。如果想使用RestTemplate也差不多是一样的方式。

UrlRequestTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package com.lzumetal.springboot.demodatabase.test;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.lzumetal.springboot.demodatabase.StartupApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.HashMap;
import java.util.Map;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartupApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
public class UrlRequestTest {


private static Gson gson = new GsonBuilder().setPrettyPrinting().create();

@Autowired
private TestRestTemplate testRestTemplate;

/**
* GET请求+@PathVariable
*/
@Test
public void getRequest() {
ResponseEntity<String> entity = testRestTemplate.getForEntity("/getBook/{id}", String.class, 1);
System.err.println(entity.getBody());
}


/**
* GET请求
* getForEntity 和 getForObject 的区别:
* getForObject返回结果Controller中的返回类型。
* getForEntity返回结果里包含了请求头信息等,同时entity.getBody()的结果已经被转换成了json字符串
*/
@Test
public void getRequest2() {
String result = testRestTemplate.getForObject("/getBookInfo2?id={id}&name={2}", String.class, 100, "解忧杂货店");
System.err.println(result);
}


/**
* GET请求除了使用占位符的方式按次序注入,也可以通过一个map通过名字注入
*/
@Test
public void getRequest3() {
Map<String, Object> param = new HashMap<>();
param.put("bookid", 20);
param.put("name", "呼啸山庄");
String result = testRestTemplate.getForObject("/getBookInfo2?id={bookid}&name={name}", String.class, param);
System.err.println(result);
}


/**
* POST请求
*/
@Test
public void postRequest() {
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("id",2);
String result = testRestTemplate.postForObject("/postBookInfo", param, String.class);
System.err.println(result);
}

/**
* POST请求,并带请求头
*/
@Test
public void postRequest2() {
HttpHeaders headers = new HttpHeaders();
headers.add("token", "aaaaaaabbbbbbdcccc");

MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("id",2);

HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(param, headers);

ResponseEntity<String> resultEntity = testRestTemplate.postForEntity("/postBookInfo", entity, String.class);
System.err.println("reuslt:" + resultEntity.getBody());
System.err.println("headers:" + resultEntity.getHeaders());
}


/**
* POST请求,入参是json格式字符串:{"id":2,"name":"Effective Java","author":"Joshua Bloch","price":39.0}
*/
@Test
public void postRequest3() {
String jsonStr = "{\"id\":2,\"name\":\"Effective Java\",\"author\":\"Joshua Bloch\",\"price\":39.0}";
HttpHeaders headers = new HttpHeaders();

//设置contentType
headers.setContentType(MediaType.valueOf("application/json;UTF-8"));

HttpEntity<String> entity = new HttpEntity<String>(jsonStr,headers);
String result = testRestTemplate.postForObject("/postJson", entity, String.class);
System.err.println(result);
}

/**
* 上传文件
*
* @throws Exception
*/
@Test
public void upload() throws Exception {
Resource resource = new FileSystemResource("d:/123.jpg");
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("files", resource);
String result = testRestTemplate.postForObject("/uploadFile", param, String.class);
System.out.println(result);
}


/**
* 下载文件
*
* @throws Exception
*/
@Test
public void download() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.set("token", "xxxxxx");
HttpEntity formEntity = new HttpEntity(headers);

ResponseEntity<byte[]> response = testRestTemplate.exchange("/download?file={1}", HttpMethod.GET, formEntity, byte[].class, "d:/aaa.png");
if (response.getStatusCode() == HttpStatus.OK) {
FileUtils.writeByteArrayToFile(new File("d:/123.jpg"), response.getBody());
}
}


}

官网上的说明:

Note that TestRestTemplate is now available as bean whenever @SpringBootTest is used. It’s pre-configured to resolve relative paths to http://localhost:${local.server.port}. We could have also used the @LocalServerPort annotation to inject the actual port that the server is running on into a test field.

关于webEnvironment,有MOCK、RANDOM_PORT、DEFINED_PORT、NONE四个选项,其中:

  • MOCK —提供一个虚拟的Servlet环境,内置的Servlet容器并没有真实的启动
  • RANDOM_PORT — 提供一个真实的Servlet环境,也就是说会启动内置容器,然后使用的是随机端口
  • DEFINED_PORT — 这个配置也是提供一个真实的Servlet环境,使用的配置文件中配置的端口,如果没有配置,默认是8080

关于SpringBoot的单元测试,可能也还会用到@Before等其它注解,本文不再全面而深入的研究,仅展示简单示例供使用参考。


本文示例代码已上传到GitHub: https://github.com/liaosilzu2007/spring-boot.git

------ 本文完 ------